﻿HmJS.$Import('core.element');

HmJS.register('core.request', function ($ns) {

	var document = window.document;

	// #region -- Request --

	/*
	---

	name: Request

	description: Powerful all purpose Request Class. Uses XMLHTTPRequest.

	license: MIT-style license.

	requires: [Object, Element, Chain, Events, Options, Browser]

	provides: Request

	...
	*/

	// 参考我佛山人关于mootools1.2-Request的源码分析(http://space.flash8.net/space/?18713/viewspace-406767.html)
	(function () {

		var empty = function () { },
				progressSupport = ('onprogress' in new Browser.Request);

		var Request = this.Request = new Class({

			// 继承Chain,Events和Options的接口实现
			Implements: [Chain, Events, Options],

			options: {/*
			onRequest: function(){}, // 开始事件
			onLoadstart: function(event, xhr){},
			onProgress: function(event, xhr){},
			onComplete: function(){},
			onCancel: function(){},
			onSuccess: function(responseText, responseXML){}, // 成功事件
			onFailure: function(xhr){}, // 失败事件
			onException: function(headerName, value){}, // 异常事件
			onTimeout: function(){},
			user: '',
			password: '',*/
				url: '', // Ajax请求的地址
				data: '', // Ajax提交的地址
				headers: { // Ajax请求的HTTP头数据
					// 来源信息
					'X-Requested-With': 'XMLHttpRequest',
					// 接受的内容类型
					'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
				},
				// 是否异步请求
				async: true,
				format: false,
				// 提交方式
				method: 'post',
				// 多次请求冲突时的处理方式
				link: 'ignore',
				// 判断状态成功的方法
				isSuccess: null,
				// 判断状态成功的方法
				emulation: true,
				// 是否使用url编码
				urlEncoded: true,
				// POST请求时使用的编码
				encoding: 'utf-8',
				// 动态执行字符脚本
				evalScripts: false,
				// 动态执行返回字符脚本
				evalResponse: false,
				timeout: 0,
				noCache: false
			},

			// 构造函数
			initialize: function (options) {
				// 使用浏览器特性创建一个XmlHttpRequest对象
				this.xhr = new Browser.Request();
				this.setOptions(options);
				this.headers = this.options.headers;
			},

			onStateChange: function () {
				// 使用局部变量缓存this.xhr对象
				var xhr = this.xhr;
				// 如果状态码不等于4或者没有正执行的请求，直接返回
				if (xhr.readyState != 4 || !this.running) { return; }
				// 设置标记
				this.running = false;
				// 初始化状态码
				this.status = 0;
				// 设置状态码
				Function.attempt(function () {
					var status = xhr.status;
					this.status = (status == 1223) ? 204 : status;
				} .bind(this));
				// 置空状态改变事件监听
				xhr.onreadystatechange = empty;
				if (progressSupport) {
					xhr.onprogress = xhr.onloadstart = empty;
				}
				clearTimeout(this.timer);

				// 提取响应数据
				this.response = { text: this.xhr.responseText || '', xml: this.xhr.responseXML };
				if (this.options.isSuccess.call(this, this.status)) {
					// 请求成功
					this.success(this.response.text, this.response.xml);
				} else {
					// 请求失败
					this.failure();
				}
			},

			// 判断请求状态是否成功
			isSuccess: function () {
				var status = this.status;
				// 状态码大于等于200小于300时为成功
				return (status >= 200 && status < 300);
			},

			isRunning: function () {
				return !!this.running;
			},

			// 处理返回字符中包含的脚本
			processScripts: function (text) {
				// 如果返回字符为纯脚本，直接解释执行
				if (this.options.evalResponse || (/(ecma|java)script/).test(this.getHeader('Content-type'))) {
					return Browser.exec(text);
				}
				// 混合字符时过滤出脚本，根据配置参数是否执行包含脚本
				return text.stripScripts(this.options.evalScripts);
			},

			// 请求成功
			success: function (text, xml) {
				// 触发事件
				this.onSuccess(this.processScripts(text), xml);
			},

			// 触发请求成功事件
			onSuccess: function () {
				// 同时触发请求完成和请求成功事件
				this.fireEvent('complete', arguments).fireEvent('success', arguments).callChain();
			},

			// 请求失败
			failure: function () {
				// 触发事件
				this.onFailure();
			},

			// 触发请求失败事件
			onFailure: function () {
				// 同时触发请求完成和请求失败事件
				this.fireEvent('complete').fireEvent('failure', this.xhr);
			},

			loadstart: function (event) {
				this.fireEvent('loadstart', [event, this.xhr]);
			},

			progress: function (event) {
				this.fireEvent('progress', [event, this.xhr]);
			},

			timeout: function () {
				this.fireEvent('timeout', this.xhr);
			},

			// 设置请求的HTTP头部数据
			setHeader: function (name, value) {
				this.headers[name] = value;
				return this;
			},

			// 获取响应的HTTP头部数据
			getHeader: function (name) {
				return Function.attempt(function () {
					return this.xhr.getResponseHeader(name);
				} .bind(this));
			},

			// 检查约束，类似Fx的同名方法
			check: function () {
				// 如果请求线程没有在运行中，返回真允许运行当前线程的请求
				if (!this.running) { return true; }
				// 如果当前有正运行的请求线程
				switch (this.options.link) {
					case 'cancel': // 配置了link参数为cancel时，直接取消正执行的线程，启用新线程  
						this.cancel();
						return true;

					case 'chain': // 等待当前线程执行完成再执行新线程
						this.chain(this.caller.pass(arguments, this));
						return false;
				}
				return false;
			},

			// 发送请求
			send: function (options) {
				// 检查约束
				if (!this.check(options)) { return this; }

				// 设置标记
				this.options.isSuccess = this.options.isSuccess || this.isSuccess;
				this.running = true;

				// 根据参数类型处理
				var type = typeOf(options);
				if (type == 'string' || type == 'element') {
					options = { data: options };
				}

				// 实现更灵活的使用,可以将配置参数延后在send时
				var old = this.options;
				options = Object.append({ data: old.data, url: old.url, method: old.method }, options);
				var data = options.data,
						url = String(options.url),
						method = options.method.toLowerCase();

				// 不同类型的处理
				switch (typeOf(data)) {
					case 'element': // 如果是HTML Element,读取它的值,并根据name值构成name=value的名值对
						data = document.id(data).toQueryString();
						break;

					case 'object':
					case 'hash':
						data = Object.toQueryString(data);
				}

				if (this.options.format) {
					var format = 'format=' + this.options.format;
					data = (data) ? format + '&' + data : format;
				}

				// get和post提交方式时的处理,因为Ajax不支持get和post,所以通过添加一个_method给后台处理
				if (this.options.emulation && !['get', 'post'].contains(method)) {
					var _method = '_method=' + method;
					data = (data) ? _method + '&' + data : _method;
					method = 'post';
				}

				// POST方式指定编码时的处理,需要设置HTTP头部信息
				if (this.options.urlEncoded && ['post', 'put'].contains(method)) {
					var encoding = (this.options.encoding) ? '; charset=' + this.options.encoding : '';
					this.headers['Content-type'] = 'application/x-www-form-urlencoded' + encoding;
				}

				if (!url) { url = document.location.pathname; }

				var trimPosition = url.lastIndexOf('/');
				if (trimPosition > -1 && (trimPosition = url.indexOf('#')) > -1) {
					url = url.substr(0, trimPosition);
				}

				if (this.options.noCache) {
					url += (url.contains('?') ? '&' : '?') + String.uniqueID();
				}

				// GET方式时的处理,直接将提交的数据加在请求地址后面
				if (data && method == 'get') {
					// 判断请求地址中有否包含问号
					url += (url.contains('?') ? '&' : '?') + data;
					data = null;
				}

				var xhr = this.xhr;
				if (progressSupport) {
					xhr.onloadstart = this.loadstart.bind(this);
					xhr.onprogress = this.progress.bind(this);
				}

				// 打开请求端口,注意以下代码中xhr对象的方法的执行顺序
				xhr.open(method.toUpperCase(), url, this.options.async, this.options.user, this.options.password);
				if (this.options.user && 'withCredentials' in xhr) {
					xhr.withCredentials = true;
				}

				// 监听请求的响应状态更改事件
				xhr.onreadystatechange = this.onStateChange.bind(this);

				// 设置所有HTTP头部信息
				Object.each(this.headers, function (value, key) {
					try {
						xhr.setRequestHeader(key, value);
					} catch (e) {
						// 出错时触发异常事件
						this.fireEvent('exception', [key, value]);
					}
				}, this);

				// 触发onRequest事件
				this.fireEvent('request');
				// 发送数据
				xhr.send(data);
				// 如果是同步执行,不需要事件监听,直接进到状态处理方法
				if (!this.options.async) { this.onStateChange(); }
				if (this.options.timeout) {
					this.timer = this.timeout.delay(this.options.timeout, this);
				}
				return this;
			},

			// 取消请求
			cancel: function () {
				// 如果没有正运行的线程，不必操作，直接返回
				if (!this.running) { return this; }
				// 设置标记
				this.running = false;
				// 使用局部变量缓存this.xhr对象
				var xhr = this.xhr;
				// 中止请求
				xhr.abort();
				clearTimeout(this.timer);
				// 置状态改变监听为空
				xhr.onreadystatechange = empty;
				if (progressSupport) {
					xhr.onprogress = xhr.onloadstart = empty;
				}
				// 创建新请求线程
				this.xhr = new Browser.Request();
				// 触发onCancel事件
				this.fireEvent('cancel');
				return this;
			}

		});

		// HTTP的方法处理
		var methods = {};
		// 处理用于Request继承实现的方法
		['get', 'post', 'put', 'delete', 'GET', 'POST', 'PUT', 'DELETE'].each(function (method) {
			methods[method] = function (data) {
				var object = {
					method: method
				};
				if (data != null) { object.data = data; }
				return this.send(object);
			};
		});

		Request.implement(methods);

		// 单件模式, 根据Request为Element实现的扩展
		Element.Properties.send = {

			// setter
			set: function (options) {
				var send = this.get('send').cancel();
				send.setOptions(options);
				return this;
			},

			// getter
			get: function () {
				// 先从临时对象取实例
				var send = this.retrieve('send');
				if (!send) {
					send = new Request({
						data: this,
						link: 'cancel',
						method: this.get('method') || 'post',
						url: this.get('action')
					});
					this.store('send', send);
				}
				return send;
			}

		};

		// Element的send方法
		Element.implement({

			// Element可以直接执行send方法实现Ajax请求了
			send: function (url) {
				// 读取缓存中的Request实例
				var sender = this.get('send');
				// 发送请求,Request的配置参数需要单独设置
				sender.send({ data: this, url: url || sender.options.url });
				return this;
			}

		});

	})();

	// #endregion

	// #region -- Request.HTML --

	/*
	---

	name: Request.HTML

	description: Extends the basic Request Class with additional methods for interacting with HTML responses.

	license: MIT-style license.

	requires: [Element, Request]

	provides: Request.HTML

	...
	*/

	// 响应输出为HTML的Ajax请求类
	// 参考我佛山人关于mootools1.2-Request.HTML的源码分析(http://space.flash8.net/space/?18713/viewspace-407193.html)
	Request.HTML = new Class({

		// 继承自Request类
		Extends: Request,

		options: {
			// 是否更新
			update: false,
			append: false,
			// 是否解释执行字符脚本
			evalScripts: true,
			// 是否过滤
			filter: false,
			headers: {
				Accept: 'text/html, application/xml, text/xml, */*'
			}
		},

		// 请求成功,覆盖Request类的同名方法
		success: function (text) {
			var options = this.options, response = this.response;

			// 解释出脚本,之后剩下HTML
			response.html = text.stripScripts(function (script) {
				response.javascript = script;
			});

			// 如果请求为完整的HTML页
			var match = response.html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
			if (match) { response.html = match[1]; }
			// 处理HTML中的节点
			var temp = new Element('div').set('html', response.html);

			// 绑定节点到返回的对象
			response.tree = temp.childNodes;
			response.elements = temp.getElements(options.filter || '*');

			// 如果提供过滤标签名,过滤树节点
			if (options.filter) {
				response.tree = response.elements;
			}
			// 如果指定需要更新的Element,将响应更新到Element
			if (options.update) {
				var update = document.id(options.update).empty();
				if (options.filter) {
					update.adopt(response.elements);
				} else {
					update.set('html', response.html);
				}
			} else if (options.append) {
				var append = document.id(options.append);
				if (options.filter) {
					response.elements.reverse().inject(append);
				} else {
					append.adopt(temp.getChildren());
				}
			}
			// 如果指定解释脚本
			if (options.evalScripts) { Browser.exec(response.javascript); }

			// 调用父类方法,触发成功事件
			this.onSuccess(response.tree, response.elements, response.html, response.javascript);
		}

	});

	// 为Element添加load属性的setter和getter
	Element.Properties.load = {

		// setter
		set: function (options) {
			// 从临时对象取Request.HTML
			var load = this.get('load').cancel();
			load.setOptions(options);
			return this;
		},

		// getter
		get: function () {
			// 从临时对象取Request.HTML
			var load = this.retrieve('load');
			// 如果不存在缓存
			if (!load) {
				// 新建并缓存一个Request.HTML实例
				load = new Request.HTML({ data: this, link: 'cancel', update: this, method: 'get' });
				this.store('load', load);
			}
			return load;
		}

	};

	// 为Element增加load方法
	Element.implement({

		// 加载远程页面节点到当前Element内
		load: function () {
			// 上面的准备工作合取load能得到Request.HTML实例,然后发送，注意Array.link的使用,使send的两个参数可以任意排列,并且可选
			this.get('load').send(Array.link(arguments, { data: Type.isObject, url: Type.isString }));
			return this;
		}

	});

	// #endregion

	// #region -- Request.JSON --

	/*
	---

	name: Request.JSON

	description: Extends the basic Request Class with additional methods for sending and receiving JSON data.

	license: MIT-style license.

	requires: [Request, JSON]

	provides: Request.JSON

	...
	*/

	// 响应输出为JSON的Ajax请求类
	// 参考我佛山人关于mootools1.2-Request.JSON的源码分析(http://space.flash8.net/space/?18713/viewspace-407569.html)
	Request.JSON = new Class({

		// 继承自Request
		Extends: Request,

		options: {
			/*onError: function(text, error){},*/
			// 是否强制返回的JSON为严格格式
			secure: true
		},

		// 覆盖父类构造函数
		initialize: function (options) {
			// 调用父类同名方法
			this.parent(options);
			// 设置HTTP头部信息
			Object.append(this.headers, {
				'Accept': 'application/json',
				'X-Request': 'JSON'
			});
		},

		// 请求成功，覆盖Request类的同名方法
		success: function (text) {
			var json;
			try {
				// 添加解释完的JSON对象到响应对象
				json = this.response.json = JSON.decode(text, this.options.secure);
			} catch (error) {
				this.fireEvent('error', [text, error]);
				return;
			}
			if (json == null) {
				this.onFailure();
			} else {
				// 调用父类方法，触发成功事件
				this.onSuccess(json, text);
			}
		}

	});

	// #endregion

	// #region -- Request.JSONP --

	/**
	* name: Request.JSONP
	* 
	* description: Defines Request.JSONP, a class for cross domain javascript via script injection.
	* 
	* authors:
	*		- Aaron Newton
	*		- Guillermo Rauch
	*		- Arian Stolwijk
	* 
	* requires:
	*		- Core/Element
	*		- Core/Request
	*		- MooTools.More
	* 
	* provides: [Request.JSONP]
	**/

	Request.JSONP = new Class({

		Implements: [Chain, Events, Options],

		options: {/*
		onRequest: function(src, scriptElement){},
		onComplete: function(data){},
		onSuccess: function(data){},
		onCancel: function(){},
		onTimeout: function(){},
		onError: function(){}, */
			onRequest: function (src) {
				if (this.options.log && window.console && console.log) {
					console.log('JSONP retrieving script with url:' + src);
				}
			},
			onError: function (src) {
				if (this.options.log && window.console && console.warn) {
					console.warn('JSONP ' + src + ' will fail in Internet Explorer, which enforces a 2083 bytes length limit on URIs');
				}
			},
			url: '',
			callbackKey: 'callback',
			injectScript: document.head,
			data: '',
			link: 'ignore',
			timeout: 0,
			log: false
		},

		initialize: function (options) {
			this.setOptions(options);
		},

		send: function (options) {
			if (!Request.prototype.check.call(this, options)) {
				return this;
			}
			this.running = true;

			var type = typeOf(options);
			if (type == 'string' || type == 'element') {
				options = { data: options };
			}
			options = Object.merge(this.options, options || {});

			var data = options.data;
			switch (typeOf(data)) {
				case 'element':
					data = document.id(data).toQueryString();
					break;

				case 'object': case 'hash':
					data = Object.toQueryString(data);
			}

			var index = this.index = Request.JSONP.counter++;

			var src = options.url +
				(options.url.test('\\?') ? '&' : '?') +
				(options.callbackKey) +
				'=Request.JSONP.request_map.request_' + index +
				(data ? '&' + data : '');

			if (src.length > 2083) {
				this.fireEvent('error', src);
			}

			Request.JSONP.request_map['request_' + index] = function () {
				this.success(arguments, index);
			} .bind(this);

			var script = this.getScript(src).inject(options.injectScript);
			this.fireEvent('request', [src, script]);

			if (options.timeout) {
				this.timeout.delay(options.timeout, this);
			}

			return this;
		},

		getScript: function (src) {
			if (!this.script) {
				this.script = new Element('script', {
					type: 'text/javascript',
					async: true,
					src: src
				});
			}
			return this.script;
		},

		success: function (args, index) {
			if (!this.running) { return; }
			this.clear()
					.fireEvent('complete', args).fireEvent('success', args)
					.callChain();
		},

		cancel: function () {
			if (this.running) { this.clear().fireEvent('cancel'); }
			return this;
		},

		isRunning: function () {
			return !!this.running;
		},

		clear: function () {
			this.running = false;
			if (this.script) {
				this.script.destroy();
				this.script = null;
			}
			return this;
		},

		timeout: function () {
			if (this.running) {
				this.running = false;
				this.fireEvent('timeout', [this.script.get('src'), this.script]).fireEvent('failure').cancel();
			}
			return this;
		}

	});

	Request.JSONP.counter = 0;
	Request.JSONP.request_map = {};

	// #endregion

	// #region -- Request.Queue --

	/**
	* name: Request.Queue
	* 
	* description: Controls several instances of Request and its variants to run only one request at a time.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- Core/Element
	*		- Core/Request
	*		- /Class.Binds
	* 
	* provides: [Request.Queue]
	**/

	Request.Queue = new Class({

		Implements: [Options, Events],

		Binds: ['attach', 'request', 'complete', 'cancel', 'success', 'failure', 'exception'],

		options: {/*
		onRequest: function(argsPassedToOnRequest){},
		onSuccess: function(argsPassedToOnSuccess){},
		onComplete: function(argsPassedToOnComplete){},
		onCancel: function(argsPassedToOnCancel){},
		onException: function(argsPassedToOnException){},
		onFailure: function(argsPassedToOnFailure){},
		onEnd: function(){},
		*/
			stopOnFailure: true,
			autoAdvance: true,
			concurrent: 1,
			requests: {}
		},

		initialize: function (options) {
			var requests;
			if (options) {
				requests = options.requests;
				delete options.requests;
			}
			this.setOptions(options);
			this.requests = {};
			this.queue = [];
			this.reqBinders = {};

			if (requests) { this.addRequests(requests); }
		},

		addRequest: function (name, request) {
			this.requests[name] = request;
			this.attach(name, request);
			return this;
		},

		addRequests: function (obj) {
			Object.each(obj, function (req, name) {
				this.addRequest(name, req);
			}, this);
			return this;
		},

		getName: function (req) {
			return Object.keyOf(this.requests, req);
		},

		attach: function (name, req) {
			if (req._groupSend) return this;
			['request', 'complete', 'cancel', 'success', 'failure', 'exception'].each(function (evt) {
				if (!this.reqBinders[name]) {
					this.reqBinders[name] = {};
				}
				this.reqBinders[name][evt] = function () {
					this['on' + evt.capitalize()].apply(this, [name, req].append(arguments));
				} .bind(this);
				req.addEvent(evt, this.reqBinders[name][evt]);
			}, this);
			req._groupSend = req.send;
			req.send = function (options) {
				this.send(name, options);
				return req;
			} .bind(this);
			return this;
		},

		removeRequest: function (req) {
			var name = typeOf(req) == 'object' ? this.getName(req) : req;
			if (!name && typeOf(name) != 'string') {
				return this;
			}
			req = this.requests[name];
			if (!req) { return this; }
			['request', 'complete', 'cancel', 'success', 'failure', 'exception'].each(function (evt) {
				req.removeEvent(evt, this.reqBinders[name][evt]);
			}, this);
			req.send = req._groupSend;
			delete req._groupSend;
			return this;
		},

		getRunning: function () {
			return Object.filter(this.requests, function (r) {
				return r.running;
			});
		},

		isRunning: function () {
			return !!(Object.keys(this.getRunning()).length);
		},

		send: function (name, options) {
			var q = function () {
				this.requests[name]._groupSend(options);
				this.queue.erase(q);
			} .bind(this);

			q.name = name;
			if (Object.keys(this.getRunning()).length >= this.options.concurrent || (this.error && this.options.stopOnFailure)) {
				this.queue.push(q);
			} else {
				q();
			}
			return this;
		},

		hasNext: function (name) {
			return (!name) ? !!this.queue.length : !!this.queue.filter(function (q) { return q.name == name; }).length;
		},

		resume: function () {
			this.error = false;
			(this.options.concurrent - Object.keys(this.getRunning()).length).times(this.runNext, this);
			return this;
		},

		runNext: function (name) {
			if (!this.queue.length) { return this; }
			if (!name) {
				this.queue[0]();
			} else {
				var found;
				this.queue.each(function (q) {
					if (!found && q.name == name) {
						found = true;
						q();
					}
				});
			}
			return this;
		},

		runAll: function () {
			this.queue.each(function (q) {
				q();
			});
			return this;
		},

		clear: function (name) {
			if (!name) {
				this.queue.empty();
			} else {
				this.queue = this.queue.map(function (q) {
					if (q.name != name) {
						return q;
					} else {
						return false;
					}
				}).filter(function (q) {
					return q;
				});
			}
			return this;
		},

		cancel: function (name) {
			this.requests[name].cancel();
			return this;
		},

		onRequest: function () {
			this.fireEvent('request', arguments);
		},

		onComplete: function () {
			this.fireEvent('complete', arguments);
			if (!this.queue.length) { this.fireEvent('end'); }
		},

		onCancel: function () {
			if (this.options.autoAdvance && !this.error) { this.runNext(); }
			this.fireEvent('cancel', arguments);
		},

		onSuccess: function () {
			if (this.options.autoAdvance && !this.error) { this.runNext(); }
			this.fireEvent('success', arguments);
		},

		onFailure: function () {
			this.error = true;
			if (!this.options.stopOnFailure && this.options.autoAdvance) { this.runNext(); }
			this.fireEvent('failure', arguments);
		},

		onException: function () {
			this.error = true;
			if (!this.options.stopOnFailure && this.options.autoAdvance) { this.runNext(); }
			this.fireEvent('exception', arguments);
		}

	});

	// #endregion

	// #region -- Request.Periodical --

	/*
	* name: Request.Periodical
	* 
	* description: Requests the same URL to pull data from a server but increases the intervals if no data is returned to reduce the load
	* 
	* authors:
	*		- Christoph Pojer
	* 
	* requires:
	*		- Core/Request
	*		- /MooTools.More
	* 
	* provides: [Request.Periodical]
	*/

	Request.implement({

		options: {
			initialDelay: 5000,
			delay: 5000,
			limit: 60000
		},

		startTimer: function (data) {
			var fn = function () {
				if (!this.running) { this.send({ data: data }); }
			};
			this.lastDelay = this.options.initialDelay;
			this.timer = fn.delay(this.lastDelay, this);
			this.completeCheck = function (response) {
				clearTimeout(this.timer);
				this.lastDelay = (response) ? this.options.delay : (this.lastDelay + this.options.delay).min(this.options.limit);
				this.timer = fn.delay(this.lastDelay, this);
			};
			return this.addEvent('complete', this.completeCheck);
		},

		stopTimer: function () {
			clearTimeout(this.timer);
			return this.removeEvent('complete', this.completeCheck);
		}

	});

	// #endregion

});